本系列文章已出版實體書籍:
「你的地圖會說話?WebGIS 與 JavaScript 的情感交織」(博碩文化)
WebGIS啟蒙首選✖五家地圖API✖近百個程式範例✖實用簡易口訣✖學習難度分級✖補充ES6小知識
本篇文章請搭配
[3D地圖-CesiumJS系列] 一、快速上手
[3D地圖-CesiumJS系列] 二、建立飛航軌跡及動畫
今天要來介紹CesiumJS的粒子系統。
粒子系統是什麼呢?粒子系統(Particle system)是一種圖形技術,可以把許多小圖像的集合來模擬物理現象。
當它們計算過後疊在一起時,會形成複雜的模糊對象,
可以仿造出煙霧、火、天氣現象等效果。
↓ 今天使用的粒子圖像如下,為官方SampleData中提供的smoke.png檔
↓ 將它們疊在一起仿製的煙霧圖像
那就讓我們一步一步開始吧!
在根目錄下建立一個html頁面,取名為Cesium_particle.html。
如果還不會CesiumJS專案建置的人,請參考前天的文章。
↓ 建立一個存放地圖的div
    <div id="cmap"></div>
↓ 引入Cesium.js
    <script src="../Build/Cesium/Cesium.js"></script>
↓ 引入css
    <link rel="stylesheet" href="../Build/Cesium/Widgets/widgets.css" />
↓ css讓地圖滿版
    <style>
        html,
        body,
        #cmap {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            overflow: hidden;
        }
    </style>
↓ 初始化地圖,新建一個Cesium.Viewer的物件,第一個參數存放地圖的容器Id,第二個參數為設定(選填)。
        const viewer = new Cesium.Viewer('cmap');
↓ 結果
        const start = Cesium.JulianDate.fromDate(new Date("2020-10-14T21:00:00Z"));
        const stop = Cesium.JulianDate.addSeconds(start, 100, new Cesium.JulianDate());
        viewer.clock.startTime = start.clone();
        viewer.clock.stopTime = stop.clone();
        viewer.clock.currentTime = start.clone();
        viewer.clock.clockRange = Cesium.ClockRange.LOOP_STOP;  // 結束後循環播放 
        viewer.clock.multiplier = 1;
        viewer.clock.shouldAnimate = true;
        viewer.timeline.zoomTo(start, stop);
↓ 時間軸
↓ 設定起始座標及終點座標
        const positionStart = Cesium.Cartesian3.fromDegrees(
            -75.15787310614596,
            39.97862668312678
        );
        const positionEnd = Cesium.Cartesian3.fromDegrees(
            -75.1633691390455,
            39.95355089912078
        );
↓ Cesium.SampledPositionProperty的物件提供內插計算方法,可以計算每個時間區間相對應的座標
        let position = new Cesium.SampledPositionProperty();
        position.addSample(start, positionStart);  // 填入起始時間及座標
        position.addSample(stop, positionEnd);  // 填入結束時間及座標
↓ 在地圖上新增車機模型
        let entity = viewer.entities.add({
            availability: new Cesium.TimeIntervalCollection([
                new Cesium.TimeInterval({
                    start: start,
                    stop: stop,
                }),
            ]),
            model: {
                uri: "Apps/SampleData/models/CesiumMilkTruck/CesiumMilkTruck.glb",
                minimumPixelSize: 64,
            },
            viewFrom: new Cesium.Cartesian3(-150.0, -100.0, 100.0),  // 右方150,前方100,上方100
            position: position,
            orientation: new Cesium.VelocityOrientationProperty(position),
        });
↓ 將車機實體設定在地圖上
        viewer.trackedEntity = entity;
↓ 結果
要計算粒子隨機排放,並且用4x4矩陣去儲存當下粒子集合的狀態,要使用以下CesiumJS提供的物件及方法。
↓ 排放模型計算工具
        const emitterModelMatrix = new Cesium.Matrix4();  // 4x4矩陣
        const translation = new Cesium.Cartesian3();  // 3維座標點
        const trs = new Cesium.TranslationRotationScale();  // 座標轉換
        
        // 除了3維座標外,加入旋轉角度
        const rotation = new Cesium.Quaternion();  
        
        // 一種旋轉表達方式,heading表示z軸,pitch表示y軸,roll表示x軸
        let hpr = new Cesium.HeadingPitchRoll();  
↓ 計算排放模型的陣列
        function ComputeEmitterModelMatrix() {
            hpr = Cesium.HeadingPitchRoll.fromDegrees(0.0, 0.0, 0.0, hpr);
            trs.translation = Cesium.Cartesian3.fromElements(-4.0, 0.0, 1.4, translation);
            trs.rotation = Cesium.Quaternion.fromHeadingPitchRoll(hpr, rotation);
            return Cesium.Matrix4.fromTranslationRotationScale(trs, emitterModelMatrix);
        }
↓ 新增一個物件,用來做為粒子系統的參數設定
        let viewModel = {
            emissionRate: 5.0,
            minimumParticleLife: 1.2,
            maximumParticleLife: 1.2,
            minimumSpeed: 1.0,
            maximumSpeed: 4.0,
            startScale: 1.0,
            endScale: 5.0,
        };
Cesium.ParticleSystem設定
↓ 在地圖上新增粒子系統物件
        let particleSystem = viewer.scene.primitives.add(
            new Cesium.ParticleSystem({
                image: "Apps/SampleData/smoke.png",
                startColor: Cesium.Color.LIGHTSEAGREEN.withAlpha(0.7),
                endColor: Cesium.Color.WHITE.withAlpha(0.0),
                startScale: viewModel.startScale,
                endScale: viewModel.endScale,
                minimumParticleLife: viewModel.minimumParticleLife,
                maximumParticleLife: viewModel.maximumParticleLife,
                minimumSpeed: viewModel.minimumSpeed,
                maximumSpeed: viewModel.maximumSpeed,
                imageSize: new Cesium.Cartesian2(
                    viewModel.particleSize,
                    viewModel.particleSize
                ),
                emissionRate: viewModel.emissionRate,
                bursts: [
                    new Cesium.ParticleBurst({
                        time: 5.0,
                        minimum: 10,
                        maximum: 100,
                    }),
                    new Cesium.ParticleBurst({
                        time: 10.0,
                        minimum: 50,
                        maximum: 100,
                    }),
                    new Cesium.ParticleBurst({
                        time: 15.0,
                        minimum: 200,
                        maximum: 300,
                    }),
                ],
                lifetime: 16.0,
                emitter: new Cesium.CircleEmitter(2.0),
                emitterModelMatrix: ComputeEmitterModelMatrix(),
                updateCallback: ApplyGravity,
            })
        );
↓ 粒子系統的updateCallback function
        const gravityScratch = new Cesium.Cartesian3();
        function ApplyGravity(p, dt) {
            Cesium.Cartesian3.normalize(p.position, gravityScratch);
            Cesium.Cartesian3.multiplyByScalar(gravityScratch
                                    , viewModel.gravity * dt, gravityScratch);
            p.velocity = Cesium.Cartesian3.add(p.velocity
                                            , gravityScratch, p.velocity);
        }
↓ 地圖新增視覺更新前的事件,並且在每次更新時,重新計算粒子系統的煙霧排放。
        viewer.scene.preUpdate.addEventListener(function (scene, time) {
            particleSystem.modelMatrix = entity.computeModelMatrix(time, new Cesium.Matrix4());
            particleSystem.emitterModelMatrix = ComputeEmitterModelMatrix();
        });
↓ 結果
可以用Cesium.knockout來把粒子設定參數跟dom標籤進行綁定,這邊就不贅述作法,直接使用UI面板修改粒子設定。
↓ 當scale設定很小時,煙霧範圍很小
↓ 當scale設定很大時,煙霧範圍很大
↓ gravity設定很高時,煙霧往上飄
↓ life設定很長時,粒子存在時間很久,煙霧會拉很長
↓ rate設定很快時,煙霧較為密集
假如要計算整個城市裡面的車輛廢氣排放,
或者要監測工廠黑煙排放,
活用CesiumJS的粒子系統想必能一目了然!![]()